package br.mikhas.task.application;

import static br.mikhas.util.Assert.*;

import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.regex.Pattern;

import br.mikhas.task.metadata.Holiday;
import br.mikhas.task.metadata.HolidayCalendar;

/**
 * Checks if a date match with an recurrence expresion.
 * <p>
 * A recurrence expression is a cron-like expression which defines when a task
 * should recur.
 * <p>
 * The expression format is: <tt>
 * (workday in month|+|*) (weekdays|*) (month day|*) (month|*)</tt>
 * <p>
 * Example 1: <tt>"* 2-6 10-20 *"</tt> means:
 * <ul>
 * <li>Recurs any workday in the month
 * <li>Recurs between monday(2) and friday(6)
 * <li>Recurs between 10th and 20th day
 * <li>Recurs any month
 * 
 * <p>
 * Example 2: <tt>"+ * * 12"</tt>
 * <ul>
 * <li>Recurs only on workdays
 * <li>Recurs any weekday
 * <li>Recurs any month day
 * <li>Recurs on december
 * </ul>
 * 
 * <b>NOTE:</b>
 * 
 * <pre>
 * Month numbering starts on 1 (January). Ends on 12 (December).<br>
 * Weekday numbering starts on 1 (Sunday). Ends on 7 (Saturday).
 * </pre>
 * 
 * @author Mikhail Domanoski
 * 
 */
public final class RecurrenceMatcher {

	private RecurrenceMatcher() {
	}

	/**
	 * A group of selected numbers.
	 * <p>
	 * Example: <tt>2,3,4,15,10</tt>
	 */
	private static final Pattern GROUP_PATTERN = Pattern
			.compile("\\d+(,\\d+)*");

	/**
	 * A range of numbers.
	 * <p>
	 * Example: <tt>2-10</tt>
	 */
	private static final Pattern RANGE_PATTERN = Pattern.compile("\\d+[-]\\d+");

	/**
	 * Number increment pattern
	 * <p>
	 * Example: <tt>10/3</tt>
	 */
	private static final Pattern INCREMENT_PATTERN = Pattern
			.compile("\\d+[\\x2F]\\d+");

	private static final Range WORKDAY_RANGE = new Range(1, 25);
	private static final Range WEEKDAY_RANGE = new Range(1, 7);
	private static final Range MONTHDAY_RANGE = new Range(1, 31);
	private static final Range MONTH_RANGE = new Range(1, 12);

	/**
	 * Checks if a date matches with a recurrence expression
	 * 
	 * @param date
	 *            The date to check
	 * @param expression
	 *            The expression which the date should match
	 * @return <tt>true</tt> if the date matches with the expression
	 */
	public static boolean matchDate(Date date, String expression,
			Collection<HolidayCalendar> calendars) {
		notNull(date);
		notEmpty(expression);

		if ("* * * *".equals(expression))
			return true;

		String[] parts = expression.split("\\s");

		if (parts.length != 4)
			throw new RecurrenceMatchException(
					"Invalid reccurrence expresion: " + expression);

		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		// int year = calendar.get(Calendar.YEAR);
		int month = calendar.get(Calendar.MONTH) + 1;
		int day = calendar.get(Calendar.DAY_OF_MONTH);
		int weekday = calendar.get(Calendar.DAY_OF_WEEK);

		String workdayPart = parts[0], weekdayPart = parts[1], monthdayPart = parts[2], monthPart = parts[3];

		// Workday
		if (!checkWorkday(date, calendars, weekday, workdayPart))
			return false;

		// Weekday
		if (!matchesPattern(weekdayPart, weekday))
			return false;

		// Month day
		if (!matchesPattern(monthdayPart, day))
			return false;

		// Month
		if (!matchesPattern(monthPart, month))
			return false;

		// If mathes everything, than it's true
		return true;
	}

	/**
	 * Checks if the date matches the workday requirement on the expression
	 * 
	 * @param date
	 *            The date to check
	 * @param exp
	 *            the recurrence expression part/pattern
	 * @return <tt>true</tt> if the date matches que expression workday
	 *         requirement
	 */
	private static boolean checkWorkday(Date date,
			Collection<HolidayCalendar> calendars, int weekday, String exp) {

		if (exp.charAt(0) == '+') {
			return weekday != 1 && weekday != 7 && !isHoliday(date, calendars);
		} else if (exp.charAt(0) == '*') {
			return true;
		}

		int monthWorkday = getWorkdayInMonth(date, calendars);

		return matchesPattern(exp, monthWorkday);
	}

	/**
	 * Gets the workday in month considering holidays
	 * 
	 * @param date
	 *            The date to get the workday
	 * @return The workday in the month
	 */
	private static int getWorkdayInMonth(Date date,
			Collection<HolidayCalendar> calendars) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		cal.set(Calendar.DAY_OF_MONTH, 1);

		Calendar ref = Calendar.getInstance();
		ref.setTime(date);

		int workday = 0;

		do {
			int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
			if (dayOfWeek != Calendar.SUNDAY && dayOfWeek != Calendar.SATURDAY) {
				if (!isHoliday(cal.getTime(), calendars))
					workday++;
			}
			cal.add(Calendar.DAY_OF_MONTH, 1);
		} while (ref.compareTo(cal) >= 0);

		return workday;
	}

	/**
	 * Checks if the value matches
	 * 
	 * @param exp
	 * @param value
	 * @return
	 */
	private static boolean matchesPattern(String exp, int value) {
		if (exp.charAt(0) == '*')
			return true;

		if (GROUP_PATTERN.matcher(exp).matches()) {
			return matchGroup(exp, value);
		} else if (RANGE_PATTERN.matcher(exp).matches()) {
			return matchRange(exp, value);
		} else if (INCREMENT_PATTERN.matcher(exp).matches()) {
			return matchIncrement(exp, value);
		} else
			throw new RecurrenceMatchException(
					"Recurrence expression part syntax error: " + exp);
	}

	private static boolean matchGroup(String exp, int value) {
		String[] numbers = exp.split(",");

		for (String number : numbers)
			if (Integer.parseInt(number) == value)
				return true;
		return false;
	}

	private static boolean matchIncrement(String exp, int value) {
		String[] parts = exp.split("\\x2F");
		int begin = Integer.parseInt(parts[0]);
		int increment = Integer.parseInt(parts[1]);

		do {
			if (value == begin) {
				return true;
			} else {
				begin = begin + increment;
			}
		} while (begin < 32);

		return false;
	}

	private static boolean matchRange(String exp, int value) {
		int slash = exp.indexOf('-');

		int start = Integer.parseInt(exp.substring(0, slash));
		int end = Integer.parseInt(exp.substring(slash + 1));

		return value <= end && value >= start;
	}

	private static boolean isHoliday(Date date,
			Collection<HolidayCalendar> calendars) {
		if (calendars == null || calendars.isEmpty())
			return false;
		Holiday holder = new Holiday(date);
		for (HolidayCalendar calendar : calendars) {
			if (calendar.isHoliday(holder))
				return true;
		}
		return false;
	}

	public static boolean isValidExpression(String expression) {
		notEmpty(expression);

		if ("* * * *".equals(expression))
			return true;

		String[] parts = expression.split("\\s");

		if (parts.length != 4)
			return false;

		String workdayPart = parts[0], weekdayPart = parts[1], monthdayPart = parts[2], monthPart = parts[3];

		if (!isValidWorkdayExpression(workdayPart))
			return false;

		if (!isValidPattern(weekdayPart, WEEKDAY_RANGE))
			return false;

		if (!isValidPattern(monthdayPart, MONTHDAY_RANGE))
			return false;

		if (!isValidPattern(monthPart, MONTH_RANGE))
			return false;

		return true;
	}

	private static boolean isValidWorkdayExpression(String expression) {
		char c = expression.charAt(0);
		if (c == '*' || c == '+')
			return true;
		return isValidPattern(expression, WORKDAY_RANGE);
	}

	private static boolean isValidPattern(String expression, Range range) {
		if (expression.charAt(0) == '*')
			return true;

		if (GROUP_PATTERN.matcher(expression).matches()) {
			return isValidGroupPattern(expression, range);
		} else if (RANGE_PATTERN.matcher(expression).matches()) {
			return isValidRangePattern(expression, range);
		} else if (INCREMENT_PATTERN.matcher(expression).matches()) {
			return isValidIncrementPattern(expression, range);
		} else
			return false;
	}

	private static boolean isValidGroupPattern(String exp, Range range) {
		String[] numbers = exp.split(",");

		for (String number : numbers) {
			int i = Integer.parseInt(number);
			if (!range.check(i))
				return false;
		}
		return true;
	}

	private static boolean isValidIncrementPattern(String exp, Range range) {
		String[] parts = exp.split("\\x2F");
		int begin = Integer.parseInt(parts[0]);

		if (!range.check(begin))
			return false;

		return true;
	}

	private static boolean isValidRangePattern(String exp, Range range) {
		int slash = exp.indexOf('-');

		int start = Integer.parseInt(exp.substring(0, slash));
		int end = Integer.parseInt(exp.substring(slash + 1));

		return start < end && range.check(start) && range.check(end);
	}

	private static class Range {
		int start;
		int end;

		public Range(int start, int end) {
			if (start > end) {
				int temp = start;
				start = end;
				end = temp;
			}
			this.start = start;
			this.end = end;
		}

		boolean check(int i) {
			return i >= start && i <= end;
		}
	}
}
